﻿using System;
using System.Data;
using System.Text;
using System.Configuration;
using System.Globalization;

using BizElements.Core.ProviderComponents;

namespace BizElements.Core
{
    /// <summary>Enumerates supported term types for <see cref="ContainsPredicate"/> that specify how a full-text search is to be executed.</summary>
    public enum ContainsTermType
    {
        #region SimpleTerm & PrefixTerm.

        /// <summary>Exact word or phrase.</summary>
        SimpleTerm,

        /// <summary>Word/phrase prefix. Wildcard is appended to term/pattern.</summary>
        PrefixTerm

        #endregion
    }

    #region Documentation.
    /// <summary>
    /// Performs full-text search by using specialized text indexes.
    /// </summary>
    /// <remarks>
    /// <para>ContainsPredicate cannot be created by using its constructor. Use <see cref="PredicateFactory"/> 
    /// to create predicates.</para>
    /// <para>Represents a predicate used to search columns containing character-based data types 
    /// for precise or fuzzy (less precise) matches to single words and phrases.</para>
    /// <para>CONTAINS can search for:
    /// <list type="bullet">
    /// <item>One or more specific words and/or phrases (simple term). A word is one or more characters 
    /// without spaces or punctuation. A valid phrase can consist of multiple words with spaces with or 
    /// without punctuation between them. For example, croissant is a word, and café au lait is a phrase. 
    /// Words and phrases such as these are called simple terms.</item>
    /// <item>A word or a phrase where the words begin with specified text (prefix term). In case of a 
    /// phrase, each word within the phrase is considered to be a prefix. For example, the term auto tran* 
    /// matches automatic transmission and automobile transducer.</item>
    /// </list></para>
    /// <para>CONTAINS expression is automatically rendered as a compatibile LIKE predicate if it is disabled 
    /// in the application's *.config file or if the target <see cref="DbmsType"/> is not supported. Currently,
    /// SQL Server and Oracle DBMSs are supported. The full-text search can be disabled
    /// by adding the following setting in the <b>appSettings</b> section of the application's *.config file:
    /// <code>&lt;add key="BizElements.Core.ContainsPredicate.Enabled" value="false"/&gt;</code></para>
    /// <para>CONTAINS predicate cannot be used arbitrarily. Requirements:
    /// <list type="bullet">
    /// <item>Specialized indexing service, such as <b>Microsoft Full-text Search</b> or <b>Oracle Text</b>.</item>
    /// <item>Full-text index must be created/enabled for the target column.</item>
    /// </list>
    /// </para>
    /// <seealso cref="SearchCondition"/>
    /// </remarks>
    /// <example>
    /// The following example selects all products which contain the specified word.
    /// <code>
    /// public DataTable SelectProductsWhichContainsTheWord(string word)
    /// {
    ///		ProductsMeta products = new ProductsMeta();
    ///		SelectStatement select = new SelectStatement(products);
    ///		select.Where.Add(PredicateFactory.Contains(products.ProductName, word));
    ///		return select.Execute();
    /// }
    /// </code>
    /// </example>
    #endregion
    public sealed class ContainsPredicate : PredicateBase
    {
        /// <summary>Gets term type that specifies specify how a full-text search is to be executed.</summary>
        public ContainsTermType TermType { get; private set; }

        #region CTor.

        internal ContainsPredicate(ContainsTermType type, IDbColumn field, string term, bool negate)
        {
            if (term == null)
                throw new ArgumentNullException("term", Messages.ContainsPredicate_NullIsNotAllowed);

            // throw new ArgumentException("CONTAINS predicate may only be used with textual field. " + field.ColumnName + " is not textual.", "field");
            if (!TypeUtil.IsText(field.DataType))
                throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, Messages.ContainsPredicate_FieldXIsNotTextual, field.ColumnName), "field");

            term = CleanContainsParameter(term);
            if (term.Length == 0)
                throw new ArgumentException(Messages.ContainsPredicate_TermIsNotValid, "term");

            this.TermType = type;
            PredicateItems = new PredicateItem[] 
            { 
                new PredicateItem(field), 
                new PredicateItem(term, DbType.String, field.MaxLengthIfText, field.GetPrefixedAlias()) 
            };
            Negate = negate;
        }

        private static string CleanContainsParameter(string dirty)
        {
            StringBuilder clean = new StringBuilder(dirty.Length);
            foreach (char token in dirty)
            {
                bool isValid = char.IsLetterOrDigit(token) || (token == '_');
                if (isValid)
                {
                    clean.Append(token);
                }
                else
                {
                    bool lastCharIsNotSpace = (clean.Length == 0) || (clean[clean.Length - 1] != ' ');
                    if (lastCharIsNotSpace)
                        clean.Append(' ');
                }
            }

            return clean.ToString().Trim();
        }

        #endregion

        #region Render.

        /// <summary>
        /// Renders predicate as SQL statement.
        /// </summary>
        /// <param name="dbms">Target DBMS.</param>
        /// <param name="output">StringBuilder to which SQL is appended.</param>
        /// <param name="parameters">SQL parameter collection to which the object's and its children's
        /// parameters are added. After the rendering is done the collection contains all parameters with unique names.</param>
        public override void Render(DbmsType dbms, StringBuilder output, DbParameterCollection parameters)
        {
            IContainsPredicateRenderer renderer = DbmsComponentFactory.GetComponent<IContainsPredicateRenderer>(dbms);
            renderer.Render(this, dbms, output, parameters);
        }

        #endregion
    }
}
